/*  Starshatter OpenSource Distribution
    Copyright (c) 1997-2004, Destroyer Studios LLC.
    All Rights Reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name "Destroyer Studios" nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

    SUBSYSTEM:    Stars.exe
    FILE:         NavAI.cpp
    AUTHOR:       John DiCamillo


    OVERVIEW
    ========
    Automatic Navigator Artificial Intelligence class
*/

#include "MemDebug.h"
#include "NavAI.h"
#include "TacticalAI.h"
#include "Instruction.h"
#include "NavSystem.h"
#include "QuantumDrive.h"
#include "Ship.h"
#include "ShipDesign.h"
#include "ShipCtrl.h"
#include "Drive.h"
#include "Farcaster.h"
#include "Shield.h"
#include "Sim.h"
#include "StarSystem.h"
#include "KeyMap.h"
#include "HUDView.h"
#include "HUDSounds.h"

#include "Game.h"

// +----------------------------------------------------------------------+

NavAI::NavAI(Ship* s)
: ShipAI(s), complete(false), brakes(0),
drop_state(0), quantum_state(0), farcaster(0), terrain_warning(false)
{
    seek_gain = 20;
    seek_damp = 0.55;

    delete tactical;
    tactical = 0;
}

// +--------------------------------------------------------------------+

NavAI::~NavAI()
{ }

// +--------------------------------------------------------------------+

void
NavAI::ExecFrame(double s)
{
    if (!ship) return;

    seconds = s;

    ship->SetDirectorInfo(" ");

    if (ship->GetFlightPhase() == Ship::TAKEOFF)
    takeoff = true;

    else if (takeoff && ship->MissionClock() > 10000)
    takeoff = false;

    FindObjective();
    Navigator();

    // watch for disconnect:
    if (ShipCtrl::Toggled(KEY_AUTO_NAV)) {
        NavSystem* navsys = ship->GetNavSystem();
        if (navsys) {
            HUDView::GetInstance()->SetHUDMode(HUDView::HUD_MODE_TAC);
            navsys->DisengageAutoNav();

            Sim* sim = Sim::GetSim();
            if (sim) {
                ship->SetControls(sim->GetControls());
                return;
            }
        }
    }

    static double time_til_change = 0.0;

    if (time_til_change < 0.001) {
        if (ship->GetShield()) {
            Shield* shield = ship->GetShield();
            double  level  = shield->GetPowerLevel();

            if (ShipCtrl::KeyDown(KEY_SHIELDS_FULL)) {
                HUDSounds::PlaySound(HUDSounds::SND_SHIELD_LEVEL);
                shield->SetPowerLevel(100);
                time_til_change = 0.5f;
            }

            else if (ShipCtrl::KeyDown(KEY_SHIELDS_ZERO)) {
                HUDSounds::PlaySound(HUDSounds::SND_SHIELD_LEVEL);
                shield->SetPowerLevel(0);
                time_til_change = 0.5f;
            }

            else if (ShipCtrl::KeyDown(KEY_SHIELDS_UP)) {
                if (level < 25)      level =  25;
                else if (level < 50) level =  50;
                else if (level < 75) level =  75;
                else                 level = 100;

                HUDSounds::PlaySound(HUDSounds::SND_SHIELD_LEVEL);
                shield->SetPowerLevel(level);
                time_til_change = 0.5f;
            }

            else if (ShipCtrl::KeyDown(KEY_SHIELDS_DOWN)) {
                if (level > 75)      level =  75;
                else if (level > 50) level =  50;
                else if (level > 25) level =  25;
                else                 level =   0;

                HUDSounds::PlaySound(HUDSounds::SND_SHIELD_LEVEL);
                shield->SetPowerLevel(level);
                time_til_change = 0.5f;
            }

        }
    }
    else {
        time_til_change -= seconds;
    }

    if (ShipCtrl::Toggled(KEY_DECOY))
    ship->FireDecoy();

    if (ShipCtrl::Toggled(KEY_LAUNCH_PROBE))
    ship->LaunchProbe();

    if (ShipCtrl::Toggled(KEY_GEAR_TOGGLE))
    ship->ToggleGear();

    if (ShipCtrl::Toggled(KEY_NAVLIGHT_TOGGLE))
    ship->ToggleNavlights();

    if (drop_state < 0) {
        ship->DropOrbit();
        return;
    }

    if (drop_state > 0) {
        ship->MakeOrbit();
        return;
    }
}

// +--------------------------------------------------------------------+

void
NavAI::FindObjective()
{
    navpt    = 0;
    distance = 0;

    // runway takeoff:
    if (takeoff) {
        obj_w   = ship->Location()   + ship->Heading() * 10e3;
        obj_w.y = ship->Location().y + 2e3;

        // transform into camera coords:
        objective = Transform(obj_w);
        ship->SetDirectorInfo(Game::GetText("ai.takeoff"));
        return;
    }

    // PART I: Find next NavPoint:
    if (ship->GetNavSystem())
    navpt = ship->GetNextNavPoint();

    complete = !navpt;
    if (complete) return;

    // PART II: Compute Objective from NavPoint:
    Point       npt = navpt->Location();
    Sim*        sim = Sim::GetSim();
    SimRegion*  self_rgn = ship->GetRegion();
    SimRegion*  nav_rgn = navpt->Region();

    if (self_rgn && !nav_rgn) {
        nav_rgn = self_rgn;
        navpt->SetRegion(nav_rgn);
    }

    if (self_rgn == nav_rgn) {
        if (farcaster) {
            if (farcaster->GetShip()->GetRegion() != self_rgn)
            farcaster = farcaster->GetDest()->GetFarcaster();

            obj_w = farcaster->EndPoint();
        }

        else {
            obj_w = npt.OtherHand();
        }

        // distance from self to navpt:
        distance = Point(obj_w - self->Location()).length();

        // transform into camera coords:
        objective = Transform(obj_w);

        if (!ship->IsStarship())
        objective.Normalize();

        if (farcaster && distance < 1000)
        farcaster = 0;
    }

    // PART III: Deal with orbital transitions:
    else if (ship->IsDropship()) {
        if (nav_rgn->GetOrbitalRegion()->Primary() ==
                self_rgn->GetOrbitalRegion()->Primary()) {

            Point npt = nav_rgn->Location() - self_rgn->Location();
            obj_w = npt.OtherHand();

            // distance from self to navpt:
            distance = Point(obj_w - ship->Location()).length();

            // transform into camera coords:
            objective = Transform(obj_w);

            if (nav_rgn->IsAirSpace()) {
                drop_state = -1;
            }
            else if (nav_rgn->IsOrbital()) {
                drop_state = 1;
            }
        }

        // PART IIIa: Deal with farcaster jumps:
        else if (nav_rgn->IsOrbital() && self_rgn->IsOrbital()) {
            ListIter<Ship> s = self_rgn->Ships();
            while (++s && !farcaster) {
                if (s->GetFarcaster()) {
                    const Ship* dest = s->GetFarcaster()->GetDest();
                    if (dest && dest->GetRegion() == nav_rgn) {
                        farcaster = s->GetFarcaster();
                    }
                }
            }

            if (farcaster) {
                Point apt   = farcaster->ApproachPoint(0);
                Point npt   = farcaster->StartPoint();
                double r1   = (ship->Location() - npt).length();

                if (r1 > 50e3) {
                    obj_w     = apt;
                    distance  = r1;
                    objective = Transform(obj_w);
                }

                else {
                    double r2 = (ship->Location() - apt).length();
                    double r3 = (npt - apt).length();

                    if (r1+r2 < 1.2*r3) {
                        obj_w     = npt;
                        distance  = r1;
                        objective = Transform(obj_w);
                    }
                    else {
                        obj_w     = apt;
                        distance  = r2;
                        objective = Transform(obj_w);
                    }
                }
            }
        }
    }

    // PART IV: Deal with quantum jumps:
    else if (ship->IsStarship()) {
        quantum_state = 1;

        Point npt = nav_rgn->Location() + navpt->Location();
        npt -= self_rgn->Location();
        obj_w = npt.OtherHand();

        // distance from self to navpt:
        distance = Point(obj_w - ship->Location()).length();

        // transform into camera coords:
        objective = Transform(obj_w);
    }
}

// +--------------------------------------------------------------------+

void
NavAI::Navigator()
{
    accumulator.Clear();
    magnitude = 0;
    brakes    = 0;
    hold      = false;

    if (navpt) {
        if (navpt->Status() == Instruction::COMPLETE && navpt->HoldTime() > 0) {
            ship->SetDirectorInfo(Game::GetText("ai.auto-hold"));
            hold = true;
        }
        else {
            ship->SetDirectorInfo(Game::GetText("ai.auto-nav"));
        }
    }
    else {
        ship->SetDirectorInfo(Game::GetText("ai.auto-stop"));
    }

    Accumulate(AvoidTerrain());
    Accumulate(AvoidCollision());

    if (!hold)
    accumulator = SeekTarget();

    HelmControl();
    ThrottleControl();
}

// +--------------------------------------------------------------------+

void
NavAI::HelmControl()
{
    // ----------------------------------------------------------
    // STARSHIP HELM MODE
    // ----------------------------------------------------------

    if (ship->IsStarship()) {
        ship->SetFLCSMode(Ship::FLCS_HELM);
        ship->SetHelmHeading(accumulator.yaw);

        if (accumulator.pitch > 45*DEGREES)
        ship->SetHelmPitch(45*DEGREES);

        else if (accumulator.pitch < -45*DEGREES)
        ship->SetHelmPitch(-45*DEGREES);

        else
        ship->SetHelmPitch(accumulator.pitch);
    }

    // ----------------------------------------------------------
    // FIGHTER FLCS AUTO MODE
    // ----------------------------------------------------------

    else {
        ship->SetFLCSMode(Ship::FLCS_AUTO);

        // are we being asked to flee?
        if (fabs(accumulator.yaw) == 1.0 && accumulator.pitch == 0.0) {
            accumulator.pitch  = -0.7f;
            accumulator.yaw   *= 0.25f;
        }

        self->ApplyRoll((float) (accumulator.yaw * -0.4));
        self->ApplyYaw((float) (accumulator.yaw * 0.2));

        if (fabs(accumulator.yaw) > 0.5 && fabs(accumulator.pitch) < 0.1)
        accumulator.pitch -= 0.1f;

        if (accumulator.pitch != 0)
        self->ApplyPitch((float) accumulator.pitch);

        // if not turning, roll to orient with world coords:
        if (fabs(accumulator.yaw) < 0.1) {
            Point vrt = ((Camera*) &(self->Cam()))->vrt();
            double deflection = vrt.y;
            if (deflection != 0) {
                double theta = asin(deflection/vrt.length());
                self->ApplyRoll(-theta);
            }
        }

        if (!ship->IsAirborne() || ship->AltitudeAGL() > 100)
        ship->RaiseGear();
    }

    ship->SetTransX(0);
    ship->SetTransY(0);
    ship->SetTransZ(0);
    ship->ExecFLCSFrame();
}

// +--------------------------------------------------------------------+

void
NavAI::ThrottleControl()
{
    double ship_speed = ship->Velocity() * ship->Heading();
    bool   augmenter  = false;

    if (hold) {
        throttle = 0;
        brakes   = 1;
    }

    else if (navpt) {
        double speed      = navpt->Speed();

        if (speed < 10)
        speed = 250;

        throttle = 0;

        if (Ship::GetFlightModel() > 0) {
            if (ship_speed > speed + 10)
            throttle =  old_throttle - 0.25;

            else if (ship_speed < speed - 10)
            throttle =  old_throttle + 0.25;

            else
            throttle = old_throttle;
        }

        else {
            if (ship_speed > speed+5)
            brakes   = 0.25;

            else if (ship_speed < speed-5)
            throttle = 50;
        }
    }
    else {
        throttle = 0;
        brakes   = 0.25;
    }

    if (ship->IsAirborne() && ship->Class() < Ship::LCA) {
        if (ship_speed < 250) {
            throttle = 100;
            brakes   = 0;

            if (ship_speed < 200)
            augmenter = true;
        }

        else if (throttle < 20) {
            throttle = 20;
        }
    }

    old_throttle = throttle;
    ship->SetThrottle(throttle);
    ship->SetAugmenter(augmenter);

    if (ship_speed > 1 && brakes > 0)
    ship->SetTransY(-brakes * ship->Design()->trans_y);
}

// +--------------------------------------------------------------------+

Steer
NavAI::SeekTarget()
{
    if (!ship)
    return Steer();

    if (takeoff)
    return Seek(objective);

    if (navpt) {
        if (quantum_state == 1) {
            QuantumDrive* q = ship->GetQuantumDrive();

            if (q) {
                if (q->ActiveState() == QuantumDrive::ACTIVE_READY) {
                    q->SetDestination(navpt->Region(), navpt->Location());
                    q->Engage();
                }

                else if (q->ActiveState() == QuantumDrive::ACTIVE_POSTWARP) {
                    quantum_state = 0;
                }
            }
        }

        if (distance <  2 * self->Radius()) {
            ship->SetNavptStatus(navpt, Instruction::COMPLETE);

            return Steer();
        }
        else {
            return Seek(objective);
        }
    }

    return Steer();
}

// +--------------------------------------------------------------------+

void
NavAI::Disengage()
{
    throttle = 0;
}

// +--------------------------------------------------------------------+

Point
NavAI::Transform(const Point& point)
{
    if (ship && ship->IsStarship())
    return point - self->Location();

    return SteerAI::Transform(point);
}

Steer
NavAI::Seek(const Point& point)
{
    // if ship is starship, the point is in relative world coordinates
    //   x: distance east(-)  / west(+)
    //   y: altitude down(-)  / up(+)
    //   z: distance north(-) / south(+)

    if (ship && ship->IsStarship()) {
        Steer result;

        result.yaw = atan2(point.x, point.z) + PI;

        double adjacent = sqrt(point.x * point.x + point.z * point.z);
        if (fabs(point.y) > ship->Radius() && adjacent > ship->Radius())
        result.pitch = atan(point.y / adjacent);

        if (_isnan(result.yaw))
        result.yaw = 0;

        if (_isnan(result.pitch))
        result.pitch = 0;

        return result;
    }

    return SteerAI::Seek(point);
}

Steer
NavAI::Flee(const Point& point)
{
    if (ship && ship->IsStarship()) {
        Steer result = Seek(point);
        result.yaw += PI;
        result.pitch *= -1;
        return result;
    }

    return SteerAI::Flee(point);
}

Steer
NavAI::Avoid(const Point& point, float radius)
{
    if (ship && ship->IsStarship()) {
        Steer result = Seek(point);

        if (point * ship->BeamLine() > 0)
        result.yaw -= PI/2;
        else
        result.yaw += PI/2;

        return result;
    }

    return SteerAI::Avoid(point, radius);
}

// +--------------------------------------------------------------------+

Steer
NavAI::AvoidTerrain()
{
    Steer avoid;

    terrain_warning = false;

    if (!ship || !ship->GetRegion() || !ship->GetRegion()->IsActive() ||
            takeoff || (navpt && navpt->Action() == Instruction::LAUNCH))
    return avoid;

    if (ship->IsAirborne() && ship->GetFlightPhase() == Ship::ACTIVE) {
        // too low?
        if (ship->AltitudeAGL() < 1000) {
            terrain_warning = true;
            ship->SetDirectorInfo(Game::GetText("ai.too-low"));

            // way too low?
            if (ship->AltitudeAGL() < 750) {
                ship->SetDirectorInfo(Game::GetText("ai.way-too-low"));
            }

            // where will we be?
            Point selfpt = ship->Location() + ship->Velocity() + Point(0, 10e3, 0);

            // transform into camera coords:
            Point obj = Transform(selfpt);

            // pull up!
            avoid = Seek(obj);
        }
    }

    return avoid;
}



